<script setup>
import axios from 'axios'
import { ref, defineProps, toRefs, watch, computed } from 'vue'

// 支持的事件：成功和失败、关闭
const emits = defineEmits(['success', 'fail', 'close'])
// 支持的属性：类型、生成图片URL(GET)、校验URL（POST）
const props = defineProps({
    type: {
        type: String,
        default: 'SLIDER',
    },
    generateUrl: {
        type: String,
        default: 'http://localhost:8080/captcha',
    },
    validateUrl: {
        type: String,
        default: null,
    },
    log: {
        type: Boolean,
        default: false,
    },
})

// style binding
const imgDivClasses = ref([])
const sliderMoveBtnStyle = ref({})
const sliderImgDivStyle = ref({})
const bgImgDivStyle = ref({})
const sliderMoveDivStyle = ref({})
const bottomDivStyle = ref({})
const clickSpanStyle = ref({})
const clickSpans = ref([])

// doc element binding
let bgImg = ref(null)
let bgImgDiv = ref(null)
let sliderImg = ref(null)
let sliderImgDiv = ref(null)

// element props binding
let bgImgSrc = ref('')
let sliderImgSrc = ref('')
let tipImgSrc = ref('')

// variables
let currentCaptchaConfig = {}
let currentCaptchaId = null
const clickCount = ref(0)
const isLoading = ref(false)

// 初始化
const initBasics = () => {
    sliderMoveBtnStyle.value = {}
    sliderImgDivStyle.value = {}
    bgImgDivStyle.value = {}
    sliderMoveDivStyle.value = {}
    bottomDivStyle.value = {}
    clickSpanStyle.value = {}
    bgImgSrc.value = ''
    sliderImgSrc.value = ''
    tipImgSrc.value = ''
    clickSpans.value = []
    clickCount.value = 0

    switch (props.type) {
        case 'ROTATE':
            imgDivClasses.value = ['rotate-img-div']
            break
        case 'CONCAT':
            imgDivClasses.value = ['slider-img-div']
            break
        case 'CLICK':
            imgDivClasses.value = ['']
            sliderMoveDivStyle.value['height'] = '40px'
            bottomDivStyle.value['margin-top'] = '10px'
            break
        default:
            imgDivClasses.value = ['slider-img-div']
            break
    }
}

const printLog = (...params) => {
    if (props.log) {
        console.log(JSON.stringify(params))
    }
}

const reset = () => {
    if (isLoading.value) {
        return
    }

    sliderMoveBtnStyle.value['background-position'] = '-5px 11.79625%'
    sliderMoveBtnStyle.value['transform'] = 'translate(0px, 0px)'
    sliderImgDivStyle.value['transform'] = 'translate(0px, 0px) rotate(0deg)'
    currentCaptchaId = null;
}

const close = () => emits('close')

const initConfig = ({ backgroundImage, templateImage, backgroundImageHeight, backgroundImageWidth, templateImageWidth, templateImageHeight, data }) => {
    let bgImageWidth = bgImgDiv.value?.offsetWidth
    let bgImageHeight = bgImgDiv.value?.offsetHeight
    let sliderImageWidth = sliderImgDiv.value?.offsetWidth
    let sliderImageHeight = sliderImgDiv.value?.offsetHeight
    let end = 206
    switch (props.type.toUpperCase()) {
        case 'CONCAT':
            bgImgDivStyle.value['background-image'] = `url(${backgroundImage})`
            sliderImgDivStyle.value['background-image'] = `url(${backgroundImage})`
            sliderImgDivStyle.value['background-position'] = `0px 0px`
            sliderImgDivStyle.value['height'] = `${((backgroundImageHeight - data.randomY) / backgroundImageHeight) * 159}px`
            break
        case 'CLICK':
            bgImgSrc.value = backgroundImage
            tipImgSrc.value = templateImage
            end = 0
            break
        case 'ROTATE':
        case 'SLIDER':
        default:
            bgImgSrc.value = backgroundImage
            sliderImgSrc.value = templateImage
            bgImageWidth = props.type.toUpperCase() === 'ROTATE' ? 206 : bgImageWidth
            break
    }

    currentCaptchaConfig = {
        startTime: new Date(),
        trackArr: [],
        movePercent: 0,
        bgImageWidth,
        bgImageHeight,
        sliderImageWidth,
        sliderImageHeight,
        end,
    }
}

const refreshCaptcha = () => {
    let realType = 'WORD_IMAGE_CLICK'
    if (props.type.toUpperCase() === 'CLICK') {
        clickCount.value = 0
        clickSpans.value = []
    } else {
        realType = props.type.toUpperCase()
    }

    isLoading.value = true
    axios.get(`${props.generateUrl}?type=${realType}`)
        .then(response => {
            isLoading.value = false

            const { data, status, statusText } = response
            if (status !== 200) {
                emits('fail', statusText)
                return
            }
            const { code, message } = data
            if (code !== 0 && code !== 200) {
                emits('fail', message)
                return
            }

            reset()
            currentCaptchaId = data.data.id
            initConfig(data.data.captcha)
        })
        .catch(e => {
            isLoading.value = false
            emits('fail', e)
        })
}

const refreshCaptchaAsync = async () => {
    let realType = 'WORD_IMAGE_CLICK'
    if (props.type.toUpperCase() === 'CLICK') {
        clickCount.value = 0
        clickSpans.value = []
    } else {
        realType = props.type.toUpperCase()
    }

    isLoading.value = true
    bgImgSrc.value = ''
    // tipImgSrc.value = ''
    sliderImgSrc.value = ''

    try {
        const response = await axios.get(`${props.generateUrl}?type=${realType}`)
        isLoading.value = false
        const { data, status, statusText } = response
        if (status !== 200) {
            emits('fail', statusText)
            return
        }
        const { code, message } = data
        if (code !== 0 && code !== 200) {
            emits('fail', message)
            return
        }

        reset()
        currentCaptchaId = data.data.id
        initConfig(data.data.captcha)
    } catch (e) {
        isLoading.value = false
        emits('fail', e)
    }
}

const doDown = () => sliderMoveBtnStyle.value['background-position'] = '-5px 31.0092%'

const doMove = ({ moveX, end }) => {
    sliderMoveBtnStyle.value['transform'] = `translate(${moveX}px, 0px)`
    switch (props.type.toUpperCase()) {
        case 'SLIDER':
            sliderImgDivStyle.value['transform'] = `translate(${moveX}px, 0px)`
            break
        case 'ROTATE':
            sliderImgDivStyle.value['transform'] = `rotate(${moveX / (end / 360)}deg)`
            break
        case 'CONCAT':
            sliderImgDivStyle.value['background-position-x'] = `${moveX}px`
            break
        default:
            break
    }
}

const valid = ({ bgImageWidth, bgImageHeight, sliderImageWidth, sliderImageHeight, startTime, stopTime, trackArr, }) => {
    bgImageWidth = bgImageWidth || bgImgDiv.value?.offsetWidth
    bgImageHeight = bgImageHeight || bgImgDiv.value?.offsetHeight
    sliderImageWidth = sliderImageWidth || sliderImgDiv.value?.offsetWidth
    sliderImageHeight = sliderImageHeight || sliderImgDiv.value?.offsetHeight
    let data = {
        bgImageWidth,
        bgImageHeight,
        sliderImageWidth,
        sliderImageHeight,
        startSlidingTime: startTime,
        endSlidingTime: stopTime,
        // entSlidingTime: stopTime,
        trackList: trackArr,
    }
    printLog(data)

    isLoading.value = true
    axios.post(`${props.validateUrl || props.generateUrl}?id=${currentCaptchaId}`, data)
        .then(response => {
            isLoading.value = false
            tipImgSrc.value = ''
            clickSpans.value = []
            clickCount.value = 0

            const { code, msg, data, success } = response.data
            if (success || code === 200 || code === 0) {
                emits('success', data)
            } else {
                refreshCaptchaAsync()
                emits('fail', msg)
            }
        })
        .catch(e => {
            isLoading.value = false
            tipImgSrc.value = ''
            clickSpans.value = []
            clickCount.value = 0

            emits('fail', e)
        })
}

const validAsync = async ({ bgImageWidth, bgImageHeight, sliderImageWidth, sliderImageHeight, startTime, stopTime, trackArr, }) => {
    bgImageWidth = bgImageWidth || bgImgDiv.value?.offsetWidth
    bgImageHeight = bgImageHeight || bgImgDiv.value?.offsetHeight
    sliderImageWidth = sliderImageWidth || sliderImgDiv.value?.offsetWidth
    sliderImageHeight = sliderImageHeight || sliderImgDiv.value?.offsetHeight
    let data = {
        bgImageWidth,
        bgImageHeight,
        sliderImageWidth,
        sliderImageHeight,
        startSlidingTime: startTime,
        endSlidingTime: stopTime,
        // entSlidingTime: stopTime,
        trackList: trackArr,
    }
    printLog(data)

    isLoading.value = true
    try {
        const response = await axios.post(`${props.validateUrl || props.generateUrl}?id=${currentCaptchaId}`, data)
        isLoading.value = false
        const { code, msg, data:result, success } = response.data
        if (success || code === 200 || code === 0) {
            emits('success', result)
        } else {
            emits('fail', msg)
            refreshCaptchaAsync()
        }
    } catch (e) {
        isLoading.value = false
        emits('fail', e)
    }
}

const down = event => {
    if (isLoading.value) {
        return
    }

    let targetTouches = event.originalEvent?.targetTouches || event.targetTouches
    let startX = event.pageX
    let startY = event.pageY
    if (startX === undefined) {
        startX = Math.round(targetTouches[0].pageX)
        startY = Math.round(targetTouches[0].pageY)
    }
    currentCaptchaConfig.startX = startX
    currentCaptchaConfig.startY = startY

    const { startX: pageX, startY: pageY, startTime, trackArr, } = currentCaptchaConfig
    trackArr.push({
        x: pageX - startX,
        y: pageY - startY,
        type: 'down',
        t: new Date().getTime() - startTime.getTime(),
    })
    printLog('start', startX, startY)

    // PC端
    window.addEventListener('mousemove', move)
    window.addEventListener('mouseup', up)
    // 手机端
    window.addEventListener('touchmove', move, false)
    window.addEventListener('touchend', up, false)

    doDown()
}

const move = event => {
    if (isLoading.value) {
        return
    }

    if (event instanceof TouchEvent) {
        event = event.touches[0]
    }

    let pageX = Math.round(event.pageX)
    let pageY = Math.round(event.pageY)
    const { startX, startY, startTime, end, bgImageWidth, trackArr, } = currentCaptchaConfig
    let moveX = pageX - startX
    const track = {
        x: pageX - startX,
        y: pageY - startY,
        type: 'move',
        t: new Date().getTime() - startTime.getTime(),
    }
    trackArr.push(track);

    if (moveX < 0) {
        moveX = 0
    } else if (moveX > end) {
        moveX = end
    }
    currentCaptchaConfig.moveX = moveX
    currentCaptchaConfig.movePercent = moveX / bgImageWidth
    printLog('move', track)

    doMove(currentCaptchaConfig)
}

const up = event => {
    window.removeEventListener('mousemove', move)
    window.removeEventListener('mouseup', up)
    window.removeEventListener('touchmove', move)
    window.removeEventListener('touchend', up)

    if (isLoading.value) {
        return
    }

    if (event instanceof TouchEvent) {
        event = event.changedTouches[0]
    }

    currentCaptchaConfig.stopTime = new Date()
    let pageX = Math.round(event.pageX)
    let pageY = Math.round(event.pageY)
    const { startX, startY, startTime, trackArr, } = currentCaptchaConfig
    const track = {
        x: pageX - startX,
        y: pageY - startY,
        type: 'up',
        t: new Date().getTime() - startTime.getTime(),
    }
    trackArr.push(track)
    printLog('up', track)

    validAsync(currentCaptchaConfig)
}

const clickMove = event => {
    if (event instanceof TouchEvent) {
        event = event.touches[0];
    }
    currentCaptchaConfig.trackArr.push({
        x: event.offsetX,
        y: event.offsetY,
        t: new Date().getTime() - currentCaptchaConfig.startTime.getTime(),
        type: 'move'
    })
}

const click = event => {
    if (isLoading.value) {
        return
    }

    const { trackArr, startTime, } = currentCaptchaConfig
    clickCount.value++;
    if (clickCount.value === 1) {
        currentCaptchaConfig.startTime = new Date()
        window.addEventListener('mousemove', clickMove)
    }
    trackArr.push({
        x: event.offsetX,
        y: event.offsetY,
        type: 'click',
        t: new Date().getTime() - startTime.getTime(),
    })
    const left = event.offsetX - 10
    const top = event.offsetY - 10
    clickSpanStyle.value['left'] = `${left}px`
    clickSpanStyle.value['top'] = `${top}px`

    clickSpans.value.push({
        left: `${left}px`,
        top: `${top}px`,
        clickCount: clickCount.value,
    })

    if (clickCount.value >= 4) {
        currentCaptchaConfig.stopTime = new Date()
        window.removeEventListener('mousemove', clickMove)
        validAsync(currentCaptchaConfig)
            .then(() => {
                tipImgSrc.value = ''
                clickSpans.value = []
                clickCount.value = 0
            })
    }
}

watch(props, _ => {
    initBasics()
    refreshCaptchaAsync()
    console.log('type changes:', props.type)
})

onMounted(() => {
    initBasics()
    refreshCaptchaAsync()
})
</script>

<template>
    <div class="slider rotate">
        <div class="content" @click="click">
            <div ref="bgImgDiv" class="bg-img-div" :style="bgImgDivStyle">
                <img v-if="props.type.toUpperCase() !== 'CONCAT'" ref="bgImg" :src="bgImgSrc" alt />
            </div>
            <div v-if="props.type.toUpperCase() !== 'CLICK'" ref="sliderImgDiv" :class="imgDivClasses" :style="sliderImgDivStyle">
                <img v-if="props.type.toUpperCase() !== 'CONCAT'" ref="sliderImg" :src="sliderImgSrc" alt />
            </div>
            <div v-else class="bg-click-div">
                <span v-for="{left, top, clickCount} of clickSpans" class="click-span" :key="clickCount" :style="{ left, top }">{{ clickCount }}</span>
            </div>
            <div v-if="isLoading" class="loading-icon">
                <i class="loading-animation"></i>
                <span class="loading-text">loading...</span>
            </div>
        </div>
        <div class="slider-move" :style="sliderMoveDivStyle">
            <div v-if="props.type.toUpperCase() === 'CLICK'">
                <span class="slider-move-span">请依次点击:</span><img :src="tipImgSrc" class="tip-img" />
            </div>
            <template v-else>
                <div class="slider-move-track">拖动滑块完成拼图</div>
                <div class="slider-move-btn" :style="sliderMoveBtnStyle" @mousedown="down" @touchstart="down"></div>
            </template>
        </div>
        <div class="bottom" :style="bottomDivStyle">
            <div class="close-btn" @click="close"></div>
            <div class="refresh-btn" @click="refreshCaptchaAsync"></div>
        </div>
    </div>
</template>

<style scoped>
.slider {
    background-color: #fff;
    width: 278px;
    height: 285px;
    z-index: 999;
    box-sizing: border-box;
    padding: 9px;
    border-radius: 6px;
    box-shadow: 0 0 11px 0 #999999;
    user-select: none;
}

.slider .content {
    width: 100%;
    height: 159px;
    position: relative;
}

.bg-img-div {
    width: 100%;
    height: 100%;
    position: absolute;
    transform: translate(0px, 0px);

    /* for concat */
    background-size: 100% 159px;
    background-image: none;
    background-position: 0 0;
    /* for concat and click */
    z-index: 0;
}

.slider-img-div {
    height: 100%;
    position: absolute;
    transform: translate(0px, 0px);

    /* for concat */
    width: 100%;
    background-size: 100% 159px;
    z-index: 1;
}

.bg-img-div img {
    width: 100%;
}

.slider-img-div img {
    height: 100%;
}

.slider .slider-move {
    height: 60px;
    width: 100%;
    margin: 11px 0;
    position: relative;

    /* for click */
    /* height: 40px; */
}

.slider .bottom {
    height: 19px;
    width: 100%;

    /* for click */
    /* margin-top: 10px; */
}

.refresh-btn,
.close-btn,
.slider-move-track,
.slider-move-btn {
    background: url(https://static.geetest.com/static/ant/sprite.1.2.4.png) no-repeat;
}

.refresh-btn,
.close-btn {
    display: inline-block;
}

.slider-move .slider-move-track {
    line-height: 38px;
    font-size: 14px;
    text-align: center;
    white-space: nowrap;
    color: #88949d;
    -moz-user-select: none;
    -webkit-user-select: none;
    user-select: none;
}

.slider-move .slider-move-btn {
    transform: translate(0px, 0px);
    background-position: -5px 11.79625%;
    position: absolute;
    top: -12px;
    left: 0;
    width: 66px;
    height: 66px;
}

.slider-move-btn:hover,
.close-btn:hover,
.refresh-btn:hover {
    cursor: pointer
}

.bottom .close-btn {
    width: 20px;
    height: 20px;
    background-position: 0 44.86874%;
}

.bottom .refresh-btn {
    width: 20px;
    height: 20px;
    background-position: 0 81.38425%;
}

/* for rotate */
.after {
    color: #88949d;
}

.rotate-img-div {
    height: 100%;
    position: absolute;
    transform: rotate(0deg);
    margin-left: 50px;
}

.rotate-img-div img {
    height: 100%;
}

/* for click */
.bg-click-div {
    z-index: 1;
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
}

.submit-div {
    width: 100%;
    height: 40px;
    position: relative;
    margin: 5px auto;
}

.bottom .close-btn {
    width: 20px;
    height: 20px;
    background-position: 0 44.86874%;
}

.bottom .refresh-btn {
    width: 20px;
    height: 20px;
    background-position: 0 81.38425%;
}

.tip-img {
    width: 130px;
    position: absolute;
    right: 5px;
}

.slider-move-span {
    font-size: 18px;
    display: inline-block;
    height: 40px;
    line-height: 40px;
}

.click-span {
    position: absolute;
    left: 0;
    top: 0;
    border-radius: 50px;
    background-color: #409eff;
    width: 20px;
    height: 20px;
    text-align: center;
    line-height: 20px;
    color: #fff;
    border: 2px solid #fff;
}

.submit-btn {
    height: 40px;
    width: 120px;
    line-height: 40px;
    text-align: center;
    background-color: #409eff;
    color: #fff;
    font-size: 15px;
    box-sizing: border-box;
    border: 1px solid #409eff;
    float: right;
    border-radius: 5px;
}

/* added */
@keyframes loading {
    0% {
        transform: scale(0);
    }

    33% {
        opacity: 1;
    }

    100% {
        opacity: 0;
        transform: scale(1.5);
    }
}

.loading-icon {
    width: 100%;
    height: 100%;
    position: absolute;
    z-index: 99999;
    box-sizing: border-box;

    display: flex;
    flex-direction: column;
    gap: 16px;
    align-items: center;
    justify-content: center;
}

.loading-icon span {
    font-size: 16px;
    color: white;
    padding: 2px 8px;
    background-color: rgba(133, 132, 132, 0.5);
    border-radius: 4px;
}

.loading-icon i {
    height: 50px;
    width: 50px;
    background: none;

    border: 5px solid rgba(33, 186, 251, 0.75);
    border-radius: 50%;
    animation: loading 1.5s ease infinite;
}
</style>